using System;
using System.Collections.Generic;
using System.Text;

using Mapack;

namespace A4G.Core
{
	public delegate double GapPenaltyDelegate(int gaps);

	public struct AlignmentScore
	{
		public const int diagonal = 0;
		public const int gapFirstSequence = 1;
		public const int gapSecondSequence = 2;

		public static AlignmentScore CalculateScore(
			AlignmentScore scoreDiagonal,
			AlignmentScore scoreGapinFirstSequence,
			AlignmentScore scoreGapInSecondSequence,
			IGapMethodProvider gapMethodProvider,
			ScoringMatrix scoringMatrix,
			Coordinate sequenceCoordinate,
			string sequence1,
			string sequence2)
		{
			return CalculateScore(
				new AlignmentScore[] { scoreDiagonal, scoreGapinFirstSequence, scoreGapInSecondSequence },
				gapMethodProvider,
				scoringMatrix,
				sequenceCoordinate,
				sequence1,
				sequence2);
		}

		public static AlignmentScore CalculateScore(
			AlignmentScore[] scores,
			IGapMethodProvider gapMethodProvider,
			ScoringMatrix scoringMatrix,
			Coordinate sequenceCoordinate,
			string sequence1,
			string sequence2)
		{
			char charOfSequence1 = sequence1[sequenceCoordinate.X];
			char charOfSequence2 = sequence2[sequenceCoordinate.Y];

			double[] values = new double[gapSecondSequence + 1];
			values[diagonal] = scores[diagonal].Value + scoringMatrix[charOfSequence1, charOfSequence2];
			values[gapFirstSequence] = scores[gapFirstSequence].Value + gapMethodProvider.CalculateGapPenalty(scores[gapFirstSequence].FirstSequenceGapCount + 1);
			values[gapSecondSequence] = scores[gapSecondSequence].Value + gapMethodProvider.CalculateGapPenalty(scores[gapSecondSequence].SecondSequenceGapCount + 1);

			int maxIndex = int.MinValue;
			double maxScore = Matrix.GetIndexMax(values, ref maxIndex);

			return CalculateScore(scores[maxIndex], maxIndex, maxScore, sequenceCoordinate, sequence1, sequence2);
		}

		public static AlignmentScore CalculateScore(
			AlignmentScore previousScore,
			int index,
			double value,
			Coordinate sequenceCoordinate,
			string sequence1,
			string sequence2)
		{
			return CalculateScore(previousScore, null, null, index, value, sequenceCoordinate, sequence1, sequence2, false);
		}

		public static AlignmentScore CalculateScore(
			AlignmentScore previousScore,
			IGapMethodProvider gapMethodProvider,
			ScoringMatrix scoringMatrix,
			int index,
			Coordinate sequenceCoordinate,
			string sequence1,
			string sequence2)
		{
			return CalculateScore(previousScore, gapMethodProvider, scoringMatrix, index, 0.0, sequenceCoordinate, sequence1, sequence2, true);
		}

		private static AlignmentScore CalculateScore(
			AlignmentScore previousScore,
			IGapMethodProvider gapMethodProvider,
			ScoringMatrix scoringMatrix,
			int index,
			double value,
			Coordinate sequenceCoordinate,
			string sequence1,
			string sequence2,
			bool calculate)
		{
			int firstGaps = 0;
			int secondGaps = 0;
			TracebackPointers pointer = TracebackPointers.None;
			switch (index)
			{
				case diagonal:
					Rectangle sequenceRectangle = new Rectangle(0, 0, sequence1.Length - 1, sequence2.Length - 1);
					if (!sequenceRectangle.Contains(sequenceCoordinate))
					{
						throw new ApplicationException("Incorrect sequence coordinate.");
					}

					char charOfSequence1 = sequence1[sequenceCoordinate.X];
					char charOfSequence2 = sequence2[sequenceCoordinate.Y];
					pointer = (charOfSequence1 == charOfSequence2) ? TracebackPointers.Match : TracebackPointers.Mismatch;
					if ((calculate) && (scoringMatrix != null))
					{
						value = previousScore.Value + scoringMatrix[charOfSequence1, charOfSequence2];
					}
					firstGaps = 0;
					secondGaps = 0;
					break;

				case gapFirstSequence:
					pointer = TracebackPointers.GapInFirstSequence;
					if (previousScore.Pointer == TracebackPointers.GapInFirstSequence)
					{
						firstGaps = previousScore.FirstSequenceGapCount + 1;
					}
					else
					{
						firstGaps = 1;
					}
					if ((calculate) && (gapMethodProvider != null))
					{
						value = previousScore.Value + gapMethodProvider.CalculateGapPenalty(firstGaps);
					}
					secondGaps = 0;
					break;

				case gapSecondSequence:
					pointer = TracebackPointers.GapInSecondSequence;
					if (previousScore.Pointer == TracebackPointers.GapInSecondSequence)
					{
						secondGaps = previousScore.SecondSequenceGapCount + 1;
					}
					else
					{
						secondGaps = 1;
					}
					if ((calculate) && (gapMethodProvider != null))
					{
						value = previousScore.Value + gapMethodProvider.CalculateGapPenalty(secondGaps);
					}
					firstGaps = 0;
					break;

				default:
					break;
			}

			return new AlignmentScore(value, pointer, firstGaps, secondGaps, sequenceCoordinate);
		}

		public AlignmentScore(double value, TracebackPointers pointer, int firstSequenceGapCount, int secondSequenceGapCount, Coordinate sequenceCoordinate)
		{
			Value = value;
			Pointer = pointer;
			FirstSequenceGapCount = firstSequenceGapCount;
			SecondSequenceGapCount = secondSequenceGapCount;
			SequenceCoordinate = sequenceCoordinate;
		}

		public double Value;
		public TracebackPointers Pointer;
		public int FirstSequenceGapCount;
		public int SecondSequenceGapCount;
		public Coordinate SequenceCoordinate;

		public void Reset()
		{
			Value = 0;
			Pointer = TracebackPointers.None;
			FirstSequenceGapCount = 0;
			SecondSequenceGapCount = 0;
			SequenceCoordinate = new Coordinate(0, 0);
		}

		public override string ToString()
		{
			return string.Format("{0}, {1}, {2}, {3}, {4}", Value, Pointer.ToString(), FirstSequenceGapCount, SecondSequenceGapCount, SequenceCoordinate);
		}
	}
}
